﻿/// ****************************************************************************
/// Author:	        Robin Zhu
/// Create Date:	2012-4-16
/// Purpose: 		用于用户验证的静态类
/// ****************************************************************************
/// Modify By		Date			Remark
/// Robin           2012-5-17       修改cookie的存取方式，修正“记住我”机制
/// Robin           2012-5-18       使用配置节的属性作为Cookie过期天数
/// Robin           2012-6-20       防止context.User.Identity的类型不是VarIdentity
/// Robin           2012-7-9        增加验证ticket的接口
/// Robin           2012-7-24       当HttpContext.Current为null时（wcf service），使用Thread.CurrentPrincipal
/// Robin           2012-9-27       增加获取当前Identity的属性
/// Robin           2013-8-27       从Form中读取ticket
/// Robin           2013-8-27       使用Session缓存UserSession，以提高效率
/// Robin           2013-10-24      防止HttpContext为空
/// Robin           2013-12-18      增加Service验证的方法
/// Robin           2014-2-19       使用MemoryCache缓存UserSession
/// Robin           2014-2-21       修复"记住我"机制
/// Robin           2014-3-11       使用统一的配置节
/// Robin           2014-4-30       清除Thread中的Principal
/// Robin           2014-12-31      修复当LoginUrl已/开头时丢失后面的参数的问题
/// Robin           2015-3-16       防止FormsAuthentication.SignOut抛出NullReference Exception
/// Robin           2015-3-26       增加判断ticket是否在线的方法
/// ****************************************************************************
/// 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Security.Principal;
using System.Web.Routing;
using System.Text.RegularExpressions;
using RB.Utility;
using System.Threading;
using System.IO;
using System.Xml;
using System.Runtime.Caching;
using System.Diagnostics;
using System.Collections.Specialized;
using System.Reflection;
using System.Net;

namespace RB.Web.Security
{
    public static class VarAuthentication
    {
        /// <summary>
        /// 用于管理UserSession缓存
        /// </summary>
        private class UserSessionCache : MemoryCache, IDisposable
        {
            public UserSessionCache()
                : base("UserSessionCache")
            {
            }

            public void Set(string ticket, UserSession userSession, int userIsOnlineTimeWindow)
            {
                DateTimeOffset expireTime = (userIsOnlineTimeWindow > 0 ? DateTimeOffset.Now.AddMinutes(userIsOnlineTimeWindow) : InfiniteAbsoluteExpiration);
                CacheItemPolicy policy = new CacheItemPolicy();
                policy.AbsoluteExpiration = expireTime;
                policy.RemovedCallback = new CacheEntryRemovedCallback(this.RemovedCallback);

                _userSessionCache.Set(ticket, userSession, policy);
            }

            private void RemovedCallback(CacheEntryRemovedArguments arguments)
            {
                if (arguments.RemovedReason == CacheEntryRemovedReason.Expired)
                {
                    SignOut(HttpContext.Current, arguments.CacheItem.Key);
                }
            }
        }

        //private static VarAuthenticationSection _config = null;
        private static IAuthenticateService _authenticateService = null;

        private static UserSessionCache _userSessionCache = new UserSessionCache();

        static VarAuthentication()
        {
            //_config = (VarAuthenticationSection)ConfigurationManager.GetSection("RB.Authentication");
            var config = SecurityConfigSection.Current;

            Type serviceType = Type.GetType(config.AuthenticateServiceType);
            _authenticateService = (IAuthenticateService)Activator.CreateInstance(serviceType);
        }

        /// <summary>
        /// 验证当前Context，从Context中的Request或Cookie中获取ticket
        /// 验证成功则设置当前的Identity
        /// </summary>
        /// <param name="context">当前HttpContext</param>
        /// <returns>当验证成功则返回真，否则返回假</returns>
        public static bool Authenticate(HttpContext context)
        {
            bool ipfailed = false;
            try
            {
                int userIsOnlineTimeWindow = Membership.UserIsOnlineTimeWindow;
                string ticket = null;
                if (context != null)
                {
                    if (context.Request != null)
                    {
                        HttpRequest request = context.Request;
                        if (request.ServerVariables["HTTP_SOAPACTION"] != null)
                        {
                            // 当前调用的是WCF接口时
                            try
                            {
                                Stream HttpStream = request.InputStream;
                                var strmpos = HttpStream.Position;
                                HttpStream.Position = 0;
                                XmlDocument dom = new XmlDocument();
                                dom.Load(HttpStream);
                                var list = dom.GetElementsByTagName("ticket");
                                if (list != null && list.Count > 0)
                                {
                                    ticket = list[0].InnerText;
                                }
                                HttpStream.Position = strmpos;
                            }
                            catch
                            {
                            }
                        }
                        else
                        {
                            if (request.QueryString != null)
                            {
                                ticket = request.QueryString["ticket"];
                            }
                            if (string.IsNullOrEmpty(ticket) && request.Form != null)
                            {
                                ticket = request.Form["ticket"];
                            }
                            Debug.Print("VarAuthentication: ticket=" + ticket);
                            if (string.IsNullOrEmpty(ticket))
                            {
                                var config = SecurityConfigSection.Current;
                                var cookie = request.Cookies[config.CookieName];
                                if (cookie != null)
                                {
                                    ticket = cookie.Values["ticket"];

                                    request.QueryString.SetReadOnlyItemValue("ticket", ticket);

                                    if (cookie.Values["rememberMe"] == "true")
                                    userIsOnlineTimeWindow = 0;
                                }
                            }
                            else
                            {
                                if (request.QueryString != null && !string.IsNullOrEmpty(request.QueryString["rememberMe"]))
                                {
                                    if (request.QueryString["rememberMe"] == "true")
                                    {
                                        userIsOnlineTimeWindow = 0;
                                    }
                                }
                            }
                            ipfailed = !AuthenticateIPAddress(ticket, context);
                        }
                        if (!string.IsNullOrEmpty(ticket) && userIsOnlineTimeWindow > 0)
                        {
                            var config = SecurityConfigSection.Current;
                            var cookie = request.Cookies[config.CookieName];
                            if (cookie != null && cookie.Values["ticket"] == ticket && cookie.Values["rememberMe"] == "true")
                            {
                                userIsOnlineTimeWindow = 0;
                            }
                        }
                    }
                }

                if (!ipfailed)
                {
                    bool result = Authenticate(context, ticket, userIsOnlineTimeWindow);
                    return result;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception ex)
            {
                Debug.Print("VarAuthentication Exception *****************************");
                Debug.Print(ex.Message);
                Debug.Print(ex.StackTrace);
                return false;
            }
        }

        private static bool AuthenticateIPAddress(string ticket, HttpContext context)
        {
            string ipString = context.Request.UserHostAddress;
            string httpForwarded = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

            var ipAddress = IPAddress.Parse(httpForwarded ?? ipString);
            byte[] ipBytes = ipAddress.GetAddressBytes();
            var ipCodes = HttpServerUtility.UrlTokenEncode(ipBytes);
            if (ipCodes.Length < 23)
            {
                return ticket.StartsWith(ipCodes);
            }
            else
            {
                return true;
            }
        }

        /// <summary>
        /// 验证当前Context，从参数中获取ticket
        /// 验证成功则设置当前的Identity
        /// </summary>
        /// <param name="context">当前HttpContext</param>
        /// <param name="ticket">待验证票据</param>
        /// <param name="userIsOnlineTimeWindow">判断用户是否在线的时间窗口</param>
        /// <returns>当验证成功则返回真，否则返回假</returns>
        public static bool Authenticate(HttpContext context, string ticket, int userIsOnlineTimeWindow = 0)
        {
            if (ticket != null)
            {
                UserSession userSession = (UserSession)_userSessionCache[ticket];

                if (userSession == null)
                {
                    userSession = _authenticateService.GetSession(SecurityConfigSection.Current.AppCode, ticket, userIsOnlineTimeWindow);
                }
                if (userSession != null)
                {
                    _userSessionCache.Set(ticket, userSession, userIsOnlineTimeWindow);
                    SetPrincipal(context, userSession);

                    SetAuthCookie(ticket, userIsOnlineTimeWindow == 0);

                    return true;
                }
            }
            //SignOut(context, ticket);
            return false;
        }

        public static bool Authenticate(string username, string password, bool rememberMe, string appCode = null)
        {
            if (string.IsNullOrWhiteSpace(appCode))
                appCode = SecurityConfigSection.Current.AppCode;

            HttpContext context = HttpContext.Current;

            string ipAddress = null;
            string httpForwarded = null;
            if (context != null && context.Request != null)
            {
                ipAddress = context.Request.UserHostAddress;
                httpForwarded = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
            }

            bool result = Membership.ValidateUser(username, password);

            if (result)
            {
                var userSession = _authenticateService.CreateSession(appCode, username, ipAddress, httpForwarded);

                if (userSession != null)
                {
                    SetPrincipal(context, userSession);
                }
                else
                {
                    result = false;
                }
            }
            else
            {
                _authenticateService.LogPasswordAttemp(appCode, username, password, ipAddress, httpForwarded);
            }
            return result;
        }

        private static void SetPrincipal(HttpContext context, UserSession userSession)
        {
            VarIdentity identity = new VarIdentity(userSession);
            var principal = new GenericPrincipal(identity, userSession.Roles);
            if (context != null)
            {
                context.User = principal;
            }
            else
            {
                Thread.CurrentPrincipal = principal;
            }
        }

        public static void SignOut()
        {
            HttpContext context = HttpContext.Current;

            SignOut(context);
        }

        private static void SignOut(HttpContext context)
        {
            if (context != null)
            {
                string ticket = null;
                if (context.Request.IsAuthenticated &&
                    context.User.Identity != null && context.User.Identity is VarIdentity)
                {
                    VarIdentity identity = (VarIdentity)context.User.Identity;
                    ticket = identity.Ticket;
                }
                SignOut(context, ticket);
            }
        }

        private static void SignOut(HttpContext context, string ticket)
        {
            if (context != null)
            {
                if (context.Session != null)
                {
                    context.Session.Abandon();
                }
                if (context.Response != null)
                {
                    var config = SecurityConfigSection.Current;
                    var cookie = context.Request.Cookies[config.CookieName];
                    if (cookie != null && cookie.Values["ticket"] == ticket)
                    {
                        cookie.Expires = DateTime.Now.AddDays(-1);
                        context.Response.Cookies.Add(cookie);
                    }
                }
                context.User = new GenericPrincipal(new GenericIdentity(""), null);
                if (Thread.CurrentPrincipal != null && Thread.CurrentPrincipal.Identity is VarIdentity)
                {
                    Thread.CurrentPrincipal = null;
                }
            }
            if (!string.IsNullOrEmpty(ticket))
            {
                _userSessionCache.Remove(ticket);
                UserCacheManager.Remove(ticket);
                try
                {
                    _authenticateService.SignOut(ticket);
                }
                catch(InvalidOperationException)
                {

                }
            }
            if (context != null && context.Request != null)
            {
                if (HttpContext.Current.Request.Browser == null)
                {
                    HttpContext.Current.Request.Browser = new HttpBrowserCapabilities() { Capabilities = new Dictionary<string, string> { { "supportsEmptyStringInCookieValue", "false" } } };
                }
                FormsAuthentication.SignOut();
            }
        }

        public static void SetAuthCookie(string ticket, bool rememberMe)
        {
            HttpContext context = HttpContext.Current;
            if (context != null)
            {
                HttpResponse response = context.Response;

                if (response != null)
                {
                    var config = SecurityConfigSection.Current;
                    HttpCookie cookie = new HttpCookie(config.CookieName);

                    cookie.Expires = DateTime.Today.AddMonths(3);
                    cookie.Values.Add("ticket", ticket);
                    cookie.Values.Add("rememberMe", rememberMe ? "true" : "false");

                    response.Cookies.Add(cookie);
                }

                FormsAuthentication.SetAuthCookie("T:" + ticket, rememberMe);
            }
        }

        internal static void RedirectToLogin(HttpContext context)
        {
            // Don't do it if already there is ReturnUrl, already being redirected,
            // to avoid infinite redirection loop
            var config = SecurityConfigSection.Current;
            string loginUrl = config.LoginUrl;
            String strUrl = null;
            if (loginUrl.StartsWith("/"))
            {
                strUrl = context.Request.Url.PathAndQuery;
            }
            else
            {
                strUrl = context.Request.Url.AbsoluteUri;
            }

            if (strUrl.IndexOf("?ReturnUrl=", StringComparison.Ordinal) != -1
                || strUrl.IndexOf("&ReturnUrl=", StringComparison.Ordinal) != -1)
            {
                return;
            }
            strUrl = UrlUtility.RemoveQuery(strUrl, "ticket");

            string strRedirect;
            if (loginUrl.IndexOf('?') >= 0)
            {
                strRedirect = loginUrl + "&" + "ReturnUrl=" + HttpUtility.UrlEncode(strUrl, context.Request.ContentEncoding);
            }
            else
            {
                strRedirect = loginUrl + "?" + "ReturnUrl=" + HttpUtility.UrlEncode(strUrl, context.Request.ContentEncoding);
            }
            strRedirect += "&appCode=" + VarAuthorization.AppCode;

            context.Response.Redirect(strRedirect, false);
        }

        internal static bool IsLoginPage(Uri url)
        {
            var config = SecurityConfigSection.Current;
            string loginUrl = config.LoginUrl;
            if (loginUrl.StartsWith("~"))
            {
                loginUrl = VirtualPathUtility.ToAbsolute(loginUrl);

            }
            return url.PathAndQuery.StartsWith(loginUrl);
        }

        internal static bool IsAccessDeniedPage(Uri url)
        {            
            var config = SecurityConfigSection.Current;
            string accessDeniedUrl = config.AccessDeniedUrl;
            if (string.IsNullOrWhiteSpace(accessDeniedUrl))
                return false;

            if (accessDeniedUrl.StartsWith("~"))
            {
                accessDeniedUrl = VirtualPathUtility.ToAbsolute(accessDeniedUrl);

            }
            return url.PathAndQuery.StartsWith(accessDeniedUrl);
        }

        public static string LoginPage
        {
            get
            {
                var config = SecurityConfigSection.Current;
                if (config != null)
                    return config.LoginUrl;
                return string.Empty;
            }
        }

        /// <summary>
        /// 获取当前HttpContext或Thread中的Identity
        /// </summary>
        public static VarIdentity CurrentIdentity
        {
            get
            {
                HttpContext context = HttpContext.Current;
                VarIdentity identity = null;
                if (context != null && context.User != null)
                {
                    identity = context.User.Identity as VarIdentity;
                }
                else if (Thread.CurrentPrincipal != null)
                {
                    identity = Thread.CurrentPrincipal.Identity as VarIdentity;
                }
                return identity ?? new VarIdentity();
            }
        }

        /// <summary>
        /// 判断指定ticket是否仍然在线
        /// </summary>
        /// <param name="ticket"></param>
        /// <returns></returns>
        public static bool IsOnline(string ticket, string appCode)
        {
            if (string.IsNullOrWhiteSpace(appCode))
                appCode = SecurityConfigSection.Current.AppCode;
            return _authenticateService.GetSession(appCode, ticket, 0) != null;
        }
    }
}
